package org.mixed.script;

import org.mixed.antlr4.MixScriptParser;
import org.mixed.antlr4.MixScriptParserBaseVisitor;
import org.mixed.script.runtime.MixScriptContext;
import org.mixed.script.runtime.MixScriptRuntime;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

import javax.script.Bindings;
import java.io.PrintStream;
import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import static org.objectweb.asm.Opcodes.*;

public class MixScriptVisitor extends MixScriptParserBaseVisitor<MixScriptVisitor> {

    private final ClassWriter cw;
    private GeneratorAdapter mg;

    private static final Handle FUNCTION_HANDLE = createBootstrapMethodHandle(FunctionCallHandle.class);

    public MixScriptVisitor() {
        cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cw.visit(V17, ACC_PUBLIC | ACC_SUPER, "HelloWorld", null, Type.getInternalName(MixScriptRuntime.class), null);
        createInitMethod();
        createExecMethod();
    }

    private void createInitMethod() {
        Method m = Method.getMethod("void <init> ()");
        GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, m, null, null, cw);
        mg.loadThis();
        mg.invokeConstructor(Type.getType(MixScriptRuntime.class), m);
        mg.returnValue();
        mg.endMethod();
    }

    private void createExecMethod() {
        Method m = Method.getMethod("Object execute (org.mixed.script.runtime.MixScriptContext)");
        mg = new GeneratorAdapter(ACC_PUBLIC, m, null, null, cw);
        mg.loadArg(0);
        mg.push(MixScriptContext.ENGINE_SCOPE);
        mg.invokeVirtual(Type.getType(MixScriptContext.class), Method.getMethod("javax.script.Bindings getBindings (int)"));
        mg.storeLocal(2, Type.getType(Bindings.class));
    }

    private static Handle createBootstrapMethodHandle(Class<?> target) {
        MethodType mt = MethodType.methodType(
                CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class);
        return new Handle(H_INVOKESTATIC, Type.getInternalName(target),
                "bootstrap", mt.toMethodDescriptorString(), false);
    }

    public MixScriptVisitor call(String methodName, int argumentCount) {
        mg.invokeDynamic(methodName, MethodType.genericMethodType(argumentCount).toMethodDescriptorString(), FUNCTION_HANDLE, argumentCount);
        return this;
    }

    public byte[] bytecode() {
        mg.push("hello world");
        mg.returnValue();
        mg.endMethod();
        cw.visitEnd();
        return cw.toByteArray();
    }

    @Override
    public MixScriptVisitor visitProgram(MixScriptParser.ProgramContext ctx) {
        visitChildren(ctx);
        return this;
    }

    @Override
    public MixScriptVisitor visitVariableStatement(MixScriptParser.VariableStatementContext ctx) {
        for (MixScriptParser.VariableDeclarationContext vCtx : ctx.variableDeclarationList().variableDeclaration()) {
            mg.loadLocal(2);
            mg.push(vCtx.assignable().getText());
            mg.push(vCtx.singleExpression().getText());
            mg.invokeVirtual(Type.getType(Bindings.class), Method.getMethod("Object put(String name, Object value)"));
        }
        return this;
    }

    @Override
    public MixScriptVisitor visitArgumentsExpression(MixScriptParser.ArgumentsExpressionContext ctx) {
        visitChildren(ctx);
        mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));
        return this;
    }

    @Override
    public MixScriptVisitor visitIdentifier(MixScriptParser.IdentifierContext ctx) {
        mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
        return this;
    }

    @Override
    public MixScriptVisitor visitArguments(MixScriptParser.ArgumentsContext ctx) {
        return super.visitArguments(ctx);
    }

    @Override
    public MixScriptVisitor visitLiteral(MixScriptParser.LiteralContext ctx) {
        mg.push(ctx.getText());
        return this;
    }

    @Override
    public MixScriptVisitor visitIdentifierName(MixScriptParser.IdentifierNameContext ctx) {
        return super.visitIdentifierName(ctx);
    }

    @Override
    public MixScriptVisitor visitReturnStatement(MixScriptParser.ReturnStatementContext ctx) {
        mg.push(ctx.expressionSequence().getText());
        return this;
    }

    @Override
    public MixScriptVisitor visitAdditiveExpression(MixScriptParser.AdditiveExpressionContext ctx) {
        return null;
    }
}
